diff options
Diffstat (limited to 'src/app/(main)/websites/[websiteId]/(reports)/goals')
5 files changed, 279 insertions, 0 deletions
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/goals/Goal.tsx b/src/app/(main)/websites/[websiteId]/(reports)/goals/Goal.tsx new file mode 100644 index 0000000..b6c4a11 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/(reports)/goals/Goal.tsx @@ -0,0 +1,99 @@ +import { Column, Dialog, Grid, Icon, ProgressBar, Row, Text } from '@umami/react-zen'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; +import { useMessages, useResultQuery } from '@/components/hooks'; +import { File, User } from '@/components/icons'; +import { ReportEditButton } from '@/components/input/ReportEditButton'; +import { Lightning } from '@/components/svg'; +import { formatLongNumber } from '@/lib/format'; +import { GoalEditForm } from './GoalEditForm'; + +export interface GoalProps { + id: string; + name: string; + type: string; + parameters: { + name: string; + type: string; + value: string; + }; + websiteId: string; + startDate: Date; + endDate: Date; +} + +export type GoalData = { num: number; total: number }; + +export function Goal({ id, name, type, parameters, websiteId, startDate, endDate }: GoalProps) { + const { formatMessage, labels } = useMessages(); + const { data, error, isLoading, isFetching } = useResultQuery<GoalData>(type, { + websiteId, + startDate, + endDate, + ...parameters, + }); + const isPage = parameters?.type === 'path'; + + return ( + <LoadingPanel data={data} isLoading={isLoading} isFetching={isFetching} error={error}> + {data && ( + <Grid gap> + <Grid columns="1fr auto" gap> + <Column gap> + <Row> + <Text size="4" weight="bold"> + {name} + </Text> + </Row> + </Column> + <Column> + <ReportEditButton id={id} name={name} type={type}> + {({ close }) => { + return ( + <Dialog + title={formatMessage(labels.goal)} + variant="modal" + style={{ minHeight: 300, minWidth: 400 }} + > + <GoalEditForm id={id} websiteId={websiteId} onClose={close} /> + </Dialog> + ); + }} + </ReportEditButton> + </Column> + </Grid> + <Row alignItems="center" justifyContent="space-between" gap> + <Text color="muted"> + {formatMessage(isPage ? labels.viewedPage : labels.triggeredEvent)} + </Text> + <Text color="muted">{formatMessage(labels.conversionRate)}</Text> + </Row> + <Row alignItems="center" justifyContent="space-between" gap> + <Row alignItems="center" gap> + <Icon>{parameters.type === 'path' ? <File /> : <Lightning />}</Icon> + <Text>{parameters.value}</Text> + </Row> + <Row alignItems="center" gap> + <Icon> + <User /> + </Icon> + <Text title={`${data?.num} / ${data?.total}`}>{`${formatLongNumber( + data?.num, + )} / ${formatLongNumber(data?.total)}`}</Text> + </Row> + </Row> + <Row alignItems="center" gap="6"> + <ProgressBar + value={data?.num || 0} + minValue={0} + maxValue={data?.total || 1} + style={{ width: '100%' }} + /> + <Text weight="bold" size="7"> + {data?.total ? Math.round((+data?.num / +data?.total) * 100) : '0'}% + </Text> + </Row> + </Grid> + )} + </LoadingPanel> + ); +} diff --git a/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalAddButton.tsx b/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalAddButton.tsx new file mode 100644 index 0000000..c85b79c --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalAddButton.tsx @@ -0,0 +1,28 @@ +import { Button, Dialog, DialogTrigger, Icon, Modal, Text } from '@umami/react-zen'; +import { useMessages } from '@/components/hooks'; +import { Plus } from '@/components/icons'; +import { GoalEditForm } from './GoalEditForm'; + +export function GoalAddButton({ websiteId }: { websiteId: string }) { + const { formatMessage, labels } = useMessages(); + + return ( + <DialogTrigger> + <Button variant="primary"> + <Icon> + <Plus /> + </Icon> + <Text>{formatMessage(labels.goal)}</Text> + </Button> + <Modal> + <Dialog + aria-label="add goal" + title={formatMessage(labels.goal)} + style={{ minWidth: 400, minHeight: 300 }} + > + {({ close }) => <GoalEditForm websiteId={websiteId} onClose={close} />} + </Dialog> + </Modal> + </DialogTrigger> + ); +} diff --git a/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalEditForm.tsx b/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalEditForm.tsx new file mode 100644 index 0000000..7f68047 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalEditForm.tsx @@ -0,0 +1,104 @@ +import { + Button, + Column, + Form, + FormButtons, + FormField, + FormSubmitButton, + Grid, + Label, + Loading, + TextField, +} from '@umami/react-zen'; +import { useMessages, useReportQuery, useUpdateQuery } from '@/components/hooks'; +import { ActionSelect } from '@/components/input/ActionSelect'; +import { LookupField } from '@/components/input/LookupField'; + +export function GoalEditForm({ + id, + websiteId, + onSave, + onClose, +}: { + id?: string; + websiteId: string; + onSave?: () => void; + onClose?: () => void; +}) { + const { formatMessage, labels } = useMessages(); + const { data } = useReportQuery(id); + const { mutateAsync, error, isPending, touch } = useUpdateQuery(`/reports${id ? `/${id}` : ''}`); + + const handleSubmit = async (formData: Record<string, any>) => { + await mutateAsync( + { ...formData, type: 'goal', websiteId }, + { + onSuccess: async () => { + if (id) touch(`report:${id}`); + touch('reports:goal'); + onSave?.(); + onClose?.(); + }, + }, + ); + }; + + if (id && !data) { + return <Loading placement="absolute" />; + } + + const defaultValues = { + name: '', + parameters: { type: 'path', value: '' }, + }; + + return ( + <Form onSubmit={handleSubmit} error={error?.message} defaultValues={data || defaultValues}> + {({ watch }) => { + const type = watch('parameters.type'); + + return ( + <> + <FormField + name="name" + label={formatMessage(labels.name)} + rules={{ required: formatMessage(labels.required) }} + > + <TextField autoFocus /> + </FormField> + <Column> + <Label>{formatMessage(labels.action)}</Label> + <Grid columns="260px 1fr" gap> + <Column> + <FormField + name="parameters.type" + rules={{ required: formatMessage(labels.required) }} + > + <ActionSelect /> + </FormField> + </Column> + <Column> + <FormField + name="parameters.value" + rules={{ required: formatMessage(labels.required) }} + > + {({ field }) => { + return <LookupField websiteId={websiteId} type={type} {...field} />; + }} + </FormField> + </Column> + </Grid> + </Column> + + <FormButtons> + <Button onPress={onClose} isDisabled={isPending}> + {formatMessage(labels.cancel)} + </Button> + <FormSubmitButton>{formatMessage(labels.save)}</FormSubmitButton> + </FormButtons> + </> + ); + }} + </Form> + ); +} diff --git a/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalsPage.tsx b/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalsPage.tsx new file mode 100644 index 0000000..ff7b49f --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalsPage.tsx @@ -0,0 +1,36 @@ +'use client'; +import { Column, Grid } from '@umami/react-zen'; +import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; +import { Panel } from '@/components/common/Panel'; +import { SectionHeader } from '@/components/common/SectionHeader'; +import { useDateRange, useReportsQuery } from '@/components/hooks'; +import { Goal } from './Goal'; +import { GoalAddButton } from './GoalAddButton'; + +export function GoalsPage({ websiteId }: { websiteId: string }) { + const { data, isLoading, error } = useReportsQuery({ websiteId, type: 'goal' }); + const { + dateRange: { startDate, endDate }, + } = useDateRange(); + + return ( + <Column gap> + <WebsiteControls websiteId={websiteId} /> + <SectionHeader> + <GoalAddButton websiteId={websiteId} /> + </SectionHeader> + <LoadingPanel data={data} isLoading={isLoading} error={error}> + {data && ( + <Grid columns={{ xs: '1fr', md: '1fr 1fr' }} gap> + {data.data.map((report: any) => ( + <Panel key={report.id}> + <Goal {...report} startDate={startDate} endDate={endDate} /> + </Panel> + ))} + </Grid> + )} + </LoadingPanel> + </Column> + ); +} diff --git a/src/app/(main)/websites/[websiteId]/(reports)/goals/page.tsx b/src/app/(main)/websites/[websiteId]/(reports)/goals/page.tsx new file mode 100644 index 0000000..b1ab691 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/(reports)/goals/page.tsx @@ -0,0 +1,12 @@ +import type { Metadata } from 'next'; +import { GoalsPage } from './GoalsPage'; + +export default async function ({ params }: { params: Promise<{ websiteId: string }> }) { + const { websiteId } = await params; + + return <GoalsPage websiteId={websiteId} />; +} + +export const metadata: Metadata = { + title: 'Goals', +}; |